module Bitmex

import HTTP
import JSON
using Formatting
using Dates
using WebSockets
using Lazy
import Reactive
const R = Reactive

import ..Model
const PriceType = Model.PriceType
const Bucket = Model.Bucket
const CandleStickChart = Model.CandleStickChart
const BitmexInterval = Model.BitmexInterval
const BitmexSymbol = String

function get_history(symbol :: BitmexSymbol,
                     interval :: BitmexInterval,
                     count :: Int
                    ) :: Vector{Bucket}

  @assert count > 0

  endpoint = "/trade/bucketed"
  query = Dict("binSize"=>interval,
               "partial"=>"true",
               "symbol"=>symbol,
               "count"=>count,
               "reverse"=>"true"
               # reversinti reikia nes kitaip gauni metu senumo bucketus
              )
  url = HTTP.URI(;
                 scheme="https",
                 host="www.bitmex.com",
                 path="/api/v1" * endpoint,
                 query=query)
  r = HTTP.get(url, ["Accept" => "application/json"],
               retry=false, redirect=false)
  json = JSON.parse(String(r.body))
  # kad bucketai prasidetu nuo seniausio gauto
  reversed_json = Iterators.reverse(json)
  buckets = map(reversed_json) do dict
    Bucket(dict["open"],
           dict["high"],
           dict["low"],
           dict["close"],
           dict["timestamp"]
          )
  end
  buckets
end

struct Trade
  price :: PriceType
  size :: Int64
  time :: DateTime
end

module WS
const BxDateTimeFormat = dateformat"y-m-dTH:M:S.sZ"

function parse_bx_datetime(timestamp :: String) :: DateTime
  DateTime(timestamp, BxDateTimeFormat)
end

function bx_insert_to_trade(msg_data) :: Trade
  time = @> msg_data["timestamp"] parse_bx_datetime
  Trade(msg_data["price"], msg_data["size"], time)
end

function has_and_eq(dict, key, val)
  if haskey(dict, key)
    dict[key] == val
  else
    false
  end
end

function is_bx_insert(msg) :: Bool
  has_and_eq(msg, "table", "trade") &&
  has_and_eq(msg, "action", "insert")
end

function is_bx_partial(msg) :: Bool
  has_and_eq(msg, "table", "trade") &&
  has_and_eq(msg, "action", "partial")
end

function is_bx_welcome_msg(msg) :: Bool
  has_and_eq(msg, "info", "Welcome to the BitMEX Realtime API.")
end

function is_bx_subscribe(msg) :: Bool
  haskey(msg, "subscribe") &&
  has_and_eq(msg, "success", true)
end

function is_bx_aux_message(msg) :: Bool
  is_bx_subscribe(msg) || is_bx_welcome_msg(msg)
end

const TradeOrNothing = Union{Trade, Nothing}

function doon(f :: Function)
  x -> begin
    f(x)
    x
  end
end

function debug_trade(t :: Trade)
  @debug t
end

function handle_bx_trade_message!(msg, trade_signal :: R.Signal{TradeOrNothing})
  push_to_signal!(trade :: Trade) = push!(trade_signal, trade)
  msg_data = msg["data"]
  @>> begin
    msg_data
    map(bx_insert_to_trade)
    map(doon(debug_trade))
    foreach(push_to_signal!)
  end
end

function feed_trades!(trade_signal :: R.Signal{TradeOrNothing})
  #trade_signal = R.Signal(TradeOrNothing, nothing)
  uri = "wss://www.bitmex.com/realtime?subscribe=trade:XBTUSD"
  WebSockets.open(uri) do ws
    i = 0
    while i < 30
      i = i + 1
      sleep(0.5)
      msg, success = WebSockets.readguarded(ws)
      if !success
        @error "listen_on_trades: not success"
        return
      end
      msg = @> msg String JSON.parse
      if is_bx_insert(msg) || is_bx_partial(msg)
        handle_bx_trade_message!(msg, trade_signal)
      elseif is_bx_aux_message(msg)
        @debug "aux message" msg
      else
        @error "ws unexpected" msg
        return
      end
    end
  end
end
end

function apply_to_bucket(bucket :: Bucket, price :: PriceType) :: Bucket
  close = price
  high = max(bucket.high, close)
  low = min(bucket.low, close)
  open = bucket.open
  end_time = bucket.end_time # bucket end time
  interval = bucket.interval
  Bucket(open,high,low,close,end_time,interval)
end

function before_bucket_end(bucket :: Bucket, trade :: Trade) :: Bool
  bucket.end_time < trade.time
end

function calc_next_end(last_partial :: Bucket) :: DateTime
  last_partial.end_time + last_partial.interval
end

function make_new_partial(last_partial :: Bucket, trade :: Trade) :: Bucket
  p = trade.price
  next_end = calc_next_end(last_partial)
  Bucket(p,p,p,p, next_end, last_partial.interval)
end

function update_partial(trade :: Trade, partial :: Bucket) :: Bucket
  if before_bucket_end(partial, trade)
    apply_to_bucket(partial, trade)
  else
    make_new_partial(partial, trade)
  end
end

function process_trades_to_partial!(symbol :: BitmexSymbol,
                                   trade :: R.Signal{Bucket},
                                   partial_signal :: R.Signal{Bucket}
                                  ) :: Nothing
  push_new_partial!(partial :: Bucket) = push!(partial_signal, partial)
  @>> begin
    R.sampleon(trade, trade, partial_signal)
    map(update_partial)
    foreach(push_new_partial!)
  end
end

end
